/**
 * Copyright (c) 2010 Basil Shikin, BugStat Project
 * 
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 * 
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package org.bugstat.common.utils;

import java.io.IOException;
import java.net.URL;
import java.util.Collection;
import java.util.Collections;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.scannotation.AnnotationDB;
import org.scannotation.ClasspathUrlFinder;

/**
 * This is a helper class that is used to load all interfaces that are annotated with given annotation. 
 *
 * @author Basil Shikin
 *
 */
public class AnnotatedClassLoader
{
    private static Log log = LogFactory.getLog( AnnotatedClassLoader.class );
    
    private final static AnnotationDB s_annotationDB;
    static
    {
        log.debug("Loading annotation database...");
        
        s_annotationDB = new AnnotationDB();
        
        scanBase( AnnotatedClassLoader.class );
    }
    
    /**
     * Retrieve all classes that bear given annotation and implement given interface
     * 
     * @param <T>            Type of classes to return.
     * @param annotation     Annotation that return classes should have. Must not be null.
     * @param interfaceType  Interface returned classes should implement. Must not be null.
     * 
     * @return All classes that implement given interface.
     */
    public static <T> Collection<T> retrieveClassesWithAnnotation( Class<?> annotation, Class<T> interfaceType )
    {
        if ( annotation == null )    throw new IllegalArgumentException("Invalid annotation specified");
        if ( interfaceType == null ) throw new IllegalArgumentException("Invalid interfaceType specified");
        
        final Map<String, Set<String>> annotationIndex  = s_annotationDB.getAnnotationIndex();
        final Set<String>              annotatedClasses = annotationIndex.get( annotation.getName());
        
        if ( annotatedClasses == null || annotatedClasses.isEmpty()) return Collections.emptySet();
        
        
        final Collection<T> result = new HashSet<T>();
        
        for ( String className : annotatedClasses )
        {
            try
            {
                final ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
                
                Class<?> loadedClass = classLoader.loadClass( className );
                
                if ( interfaceType.isAssignableFrom( loadedClass ) )
                {
                    final Object newInstance = loadedClass.newInstance();
                    
                    result.add( interfaceType.cast( newInstance ) );
                }
            }
            catch ( Throwable e )
            {
                log.error("Unable to load and instanciate class \"" + className + "\"", e );
            }
        }
        
        return result;
    }
    
    /**
     * Scan source base of given class for annotations
     * 
     * @param baseClass Base class who's source should be scanned. Must not be null.
     */
    public static void scanBase( final Class<?> baseClass )
    {
        if ( baseClass == null ) throw new IllegalArgumentException("Invalid base class specified");
        
        try
        {
            final URL url = ClasspathUrlFinder.findClassBase( baseClass );
            s_annotationDB.scanArchives(url);
        
            log.debug("Loaded classes from " + url);
        }
        catch ( IOException e )
        {
            log.error("Unable to load annotation database", e );
            
            throw new RuntimeException("Unable to load annotation database", e );
        }
    }
}
